#include "pdfviewer.h"
#include "ui_pdfviewer.h"

#ifdef WIN32
#include <windows.h>
#endif

#include <unistd.h>

#include "notewidget.h"
#include "qswiftprogressdialog.h"
#include "markedscrollbar.h"
#include "chronometer.h"

qreal const popplerDPI = 72.0;

static QColor yellowNote = QColor::fromHsv(60, 32, 255);
static QColor greenNote = QColor::fromHsv(120, 32, 255);
static QColor blueNote = QColor::fromHsv(180, 32, 255);
static QColor pinkNote = QColor::fromHsv(300, 32, 255);
static QColor redNote = QColor::fromHsv(0, 32, 255);

static QColor yellowHighlight = QColor::fromHsv(60, 64, 255);
static QColor greenHighlight = QColor::fromHsv(120, 64, 255);
static QColor blueHighlight = QColor::fromHsv(180, 64, 255);
static QColor pinkHighlight = QColor::fromHsv(300, 64, 255);
static QColor redHighlight = QColor::fromHsv(0, 64, 255);

void myPopplerDebugFunc(const QString & /*message*/, const QVariant & /*closure*/) { }

bool comparePDFTextBoxBySize(const PDFTextBox &a, const PDFTextBox &b)
{
	return a.bbox.width() * a.bbox.height() < b.bbox.width() * b.bbox.height();
}

PDFViewer::PDFViewer(QWidget *parent) :
	QWidget(parent),
	ui(new Ui::PDFViewer)
{
	Poppler::setDebugErrorFunction(myPopplerDebugFunc, QVariant::fromValue(0));

	connect(&fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onFileChanged(QString)), Qt::QueuedConnection);

	document = NULL;
	viewMode = SinglePage;
	crop = true;
	maxCropFraction = 0.2;
	minBorder = 10;
	shadowSize = 3;
	preRenderDistance = 3;
	navigationPending = false;
	navigationOffset = 0;
	firstRenderReady = false;
	abortAllRendering = false;
	delayedFileChangedTriggered = false;
	moreFileChangedTriggered = false;

	pageBBoxes_mutex = new QMutex(QMutex::Recursive);
	documentMutex = new QMutex(QMutex::Recursive);
	signalingMutex = new QMutex(QMutex::Recursive);
	rendererPageMask_mutex = new QMutex(QMutex::Recursive);
	renderCacheMutex = new QMutex(QMutex::Recursive);

	ui->setupUi(this);
	ui->scrollArea->setVerticalScrollBar(new MarkedScrollBar(Qt::Vertical));
	QDesktopWidget desktop;
	ui->scrollArea->viewport()->setMaximumSize(desktop.size());

	setMouseTracking(true);
	{
		QList<QObject*> queue;
		queue << ui->scrollArea;
		while (!queue.isEmpty()) {
			QObject *obj = queue.takeFirst();
			obj->installEventFilter(this);
			queue.append(obj->children());
		}
	}
	ui->scrollArea->viewport()->setStyleSheet("background:#999999");
	ui->scrollArea->viewport()->setAutoFillBackground(false);

	if (ui->scrollArea->horizontalScrollBar()) {
		connect(ui->scrollArea->horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(scrollValueChanged(int)));
		connect(ui->scrollArea->horizontalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(scrollRangeChanged(int,int)));
	}
	if (ui->scrollArea->verticalScrollBar()) {
		connect(ui->scrollArea->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(scrollValueChanged(int)));
		connect(ui->scrollArea->verticalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(scrollRangeChanged(int,int)));
	}

	QButtonGroup *groupViewMode = new QButtonGroup(this);
	groupViewMode->addButton(ui->btnSinglePage);
	groupViewMode->addButton(ui->btnSlideshow);
	groupViewMode->addButton(ui->btnTwoPages);

	QButtonGroup *groupTextNotes = new QButtonGroup(this);
	groupTextNotes->addButton(ui->btnNormalCursor);
	groupTextNotes->addButton(ui->btnAddTextNoteYellow);
	groupTextNotes->addButton(ui->btnAddTextNoteRed);
	groupTextNotes->addButton(ui->btnAddTextNoteGreen);
	groupTextNotes->addButton(ui->btnAddTextNoteBlue);
	groupTextNotes->addButton(ui->btnAddTextNotePink);
	groupTextNotes->addButton(ui->btnHighlightYellow);
	groupTextNotes->addButton(ui->btnHighlightRed);
	groupTextNotes->addButton(ui->btnHighlightGreen);
	groupTextNotes->addButton(ui->btnHighlightBlue);
	groupTextNotes->addButton(ui->btnHighlightPink);

	ui->btnSinglePage->setChecked(viewMode == SinglePage);
	ui->btnTwoPages->setChecked(viewMode == TwoPages);
	ui->btnSlideshow->setChecked(viewMode == Slideshow);
	ui->btnMinBorder->setChecked(crop);

	connect(this, SIGNAL(renderSync1(int)), this, SLOT(onRenderSync1(int)), Qt::QueuedConnection);
	connect(this, SIGNAL(renderSync2(int, QImage)), this, SLOT(onRenderSync2(int, QImage)), Qt::QueuedConnection);

	QTimer *timer = new QTimer(this);
	connect(timer, SIGNAL(timeout()), this, SLOT(mousePoll()));
	timer->start(250);

	connect(this, SIGNAL(showControls(bool)), this, SLOT(onShowControls(bool)));

	ui->splitter->setSizes(QList<int>() << 10 << 0);
	connect(ui->reportWidget, SIGNAL(snapBottomRequested()), this, SLOT(onReportSnapBottomRequested()));
	connect(ui->reportWidget, SIGNAL(snapRightRequested()), this, SLOT(onReportSnapRightRequested()));
	connect(ui->reportWidget, SIGNAL(textChanged(QString)), this, SLOT(onReportTextChanged(QString)));
	connect(ui->reportWidget, SIGNAL(navigateRequested(int,QPointF)), this, SLOT(onNavigationRequested(int,QPointF)));
	connect(ui->reportWidget, SIGNAL(navigateRequested(QByteArray,int,QPointF)), this, SLOT(onNavigationRequested(QByteArray,int,QPointF)));
}

PDFViewer::~PDFViewer()
{
	loadDocument(QString(), QByteArray());

	while (!runningJobs.isEmpty()) {
		clearFinishedJobs();
#ifdef WIN32
		Sleep(50);
#else
		usleep(50 * 1000);
#endif
	}

	delete ui;
}

// May run in a non-GUI thread
float getLuminance(QRgb rgb)
{
	return (0.2126 * qRed(rgb) + 0.7152 * qGreen(rgb) + 0.0722 * qBlue(rgb)) / 256.0;
}

// May run in a non-GUI thread
QRectF tightBoundingRect(QImage p, int minBorder, qreal maxCropFraction)
{
	if (p.width() <= 1 || p.height() <= 1)
		return QRectF(0, 0, 1, 1);

	const float lumThresh = 0.7;

	// xmin = the smallest X coord at which there is a non-empty pixel
	int xmin = p.width() - 1;
	for (int y = 0; y < p.height(); y++) {
		for (int x = 0; x < p.width() && x < xmin; x++) {
			if (getLuminance(p.pixel(x, y)) < lumThresh) {
				xmin = qMin(xmin, x);
			}
		}
	}
	xmin = qMax(xmin - minBorder, 0);

	// xmax = the largest X coord at which there is a non-empty pixel
	int xmax = 0;
	for (int y = 0; y < p.height(); y++) {
		for (int x = xmax; x < p.width(); x++) {
			if (getLuminance(p.pixel(x, y)) < lumThresh) {
				xmax = qMax(xmax, x);
			}
		}
	}
	xmax = qMin(xmax + minBorder, p.width() - 1);

	// ymin = the smallest Y coord at which there is a non-empty pixel
	int ymin = p.height() - 1;
	for (int x = 0; x < p.width(); x++) {
		for (int y = 0; y < p.height() && y < ymin; y++) {
			if (getLuminance(p.pixel(x, y)) < lumThresh) {
				ymin = qMin(ymin, y);
			}
		}
	}
	ymin = qMax(ymin - minBorder, 0);

	// ymax = the largest Y coord at which there is a non-empty pixel
	int ymax = 0;
	for (int x = 0; x < p.width(); x++) {
		for (int y = ymax; y < p.height(); y++) {
			if (getLuminance(p.pixel(x, y)) < lumThresh) {
				ymax = qMax(ymax, y);
			}
		}
	}
	ymax = qMin(ymax + minBorder, p.height() - 1);

	QRectF pageBBox = QRectF(xmin/(qreal)(p.width()-1), ymin/(qreal)(p.height()-1), (xmax-xmin)/(qreal)(p.width()-1), (ymax-ymin)/(qreal)(p.height()-1));

	if (pageBBox.left() > maxCropFraction) {
		pageBBox.setLeft(maxCropFraction);
	}
	if (pageBBox.right() < 1.0 - maxCropFraction) {
		pageBBox.setRight(1.0 - maxCropFraction);
	}

	return pageBBox;
}

void PDFViewer::loadDocument(QString fileName,
							 QByteArray docHash,
							 QHash<RenderCacheKey, QPair<QImage, qint64> > pageCache,
							 bool reloading)
{
	if (!fileWatcher.files().isEmpty())
		fileWatcher.removePaths(fileWatcher.files());
	if (!fileWatcher.directories().isEmpty())
		fileWatcher.removePaths(fileWatcher.directories());

	const bool profileDocLoad = false;
	Chronometer chronometer("document loading", !profileDocLoad);

	firstRenderReady = false;
	setUpdatesEnabled(false);
	{
		setAbortAllRendering(true);

		QMutexLocker locker(pageBBoxes_mutex); Q_UNUSED(locker);
		foreach (QMutex *pageMutex, pageBBoxes_pageMutex) {
			pageMutex->lock();
			// now we are sure there is nobody else working on this page
		}
		QMutexLocker lockerRendererPageMask(rendererPageMask_mutex); Q_UNUSED(lockerRendererPageMask);
		QMutexLocker lockerDoc(documentMutex); Q_UNUSED(lockerDoc);
		foreach (QMutex *pageMutex, pageBBoxes_pageMutex) {
			pageMutex->unlock();
		}
		QMutexLocker lockerRenderCache(renderCacheMutex); Q_UNUSED(lockerRenderCache);

		chronometer.showElapsedText("take mutexes");

		while (!pixPages.isEmpty()) {
			delete pixPages.last();
			pixPages.removeLast();
			pdfPageSizes.removeLast();
			delete pageBBoxes_pageMutex.last();
			pageBBoxes_pageMutex.removeLast();
			pageBBoxes.removeLast();
			rendererPageMask.removeLast();
		}
		textBoxes.clear();
		pageLinks.clear();
		pageJumpLinks.clear();
		jumpAnchor.docHash.clear();
		jumpAnchor.pageNumber = -1;
		pageShouldRender.clear();

		delete document;
		document = NULL;

		renderCache = pageCache;

		chronometer.showElapsedText("cleanup previous data");

		this->fileName = fileName;
		this->docHash = docHash;
		if (fileName.isEmpty()) {
			setUpdatesEnabled(true);
			return;
		}
		document = Poppler::Document::load(fileName);
		if (!document || document->isLocked()) {
			delete document;
			document = NULL;
			setUpdatesEnabled(true);
			return;
		}
		chronometer.showElapsedText("open file");
		document->setRenderHint(Poppler::Document::Antialiasing);
		document->setRenderHint(Poppler::Document::TextAntialiasing);
#ifdef HAS_SLIGHT_HINTING
		document->setRenderHint(Poppler::Document::TextSlightHinting);
#endif
		document->setRenderBackend(Poppler::Document::SplashBackend);

		for (int iPage = 0; iPage < document->numPages(); iPage++) {
			Poppler::Page* pdfPage = document->page(iPage);  // Document starts at page 0
			if (!pdfPage) {
				// this is a serious problem, abort and cleanup
				loadDocument(QString(), QByteArray());
				return;
			}

			// add dummy pixmap
			QLabel *label = new QLabel(ui->viewport);
			label->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
			label->setScaledContents(false);
			label->setStyleSheet("background:#ffffff");
			label->installEventFilter(this);
			label->setMouseTracking(true);
			pixPages << label;
			pdfPageSizes << pdfPage->pageSizeF();
			rendererPageMask << false;

			// bbox
			pageBBoxes << QRectF();

			QList<PDFTextBox> pageTextBoxes;

			if (!docHash.isEmpty()) {
				// We only need to know these for doccuments that support annotations
				QHash<Poppler::TextBox*, int> popplerBox2pageBox;
				QHash<int, Poppler::TextBox*> pageBox2popplerBox;
				QList<Poppler::TextBox*> boxes = pdfPage->textList();
				for (int iBox = 0; iBox < boxes.count(); iBox++) {
					Poppler::TextBox* box = boxes[iBox];
					QSizeF pageSize = pdfPageSizes[iPage];
					QRectF bboxText = box->boundingBox();
					bboxText = QRectF(bboxText.left()/pageSize.width(), bboxText.top()/pageSize.height(),
									   bboxText.width()/pageSize.width(), bboxText.height()/pageSize.height());
					PDFTextBox textBox;
					textBox.bbox = bboxText;
					textBox.pageNumber = iPage;
					textBox.id = pageTextBoxes.count();
					textBox.nextId = -1;
					textBox.text = box->text();
					textBox.isHighlighted = false;
					textBox.highlightColor = QColor(Qt::lightGray);
					pageTextBoxes << textBox;
					popplerBox2pageBox[box] = textBox.id;
					pageBox2popplerBox[textBox.id] = box;
				}
				qSort(pageTextBoxes.begin(), pageTextBoxes.end(), comparePDFTextBoxBySize);
				for (int iBox = 0; iBox < pageTextBoxes.count(); iBox++) {
					PDFTextBox &textBox = pageTextBoxes[iBox];
					int oldId = textBox.id;
					textBox.id = iBox;
					popplerBox2pageBox[pageBox2popplerBox[oldId]] = textBox.id;
				}
				for (int iBox = 0; iBox < boxes.count(); iBox++) {
					Poppler::TextBox* box = boxes[iBox];
					int iTextBox = popplerBox2pageBox[box];
					Poppler::TextBox* nextBox = box->nextWord();
					if (nextBox && popplerBox2pageBox.contains(nextBox)) {
						int iNextTextBox = popplerBox2pageBox[nextBox];
						pageTextBoxes[iTextBox].nextId = iNextTextBox;
					}
				}
				foreach (Poppler::TextBox* box, boxes) {
					delete box;
				}
			}
			textBoxes[iPage] = pageTextBoxes;

			foreach (Poppler::Link *link, pdfPage->links()) {
				if (link->linkType() == Poppler::Link::Browse) {
					Poppler::LinkBrowse *linkBrowse = dynamic_cast<Poppler::LinkBrowse*>(link);
					if (linkBrowse) {
						pageLinks[iPage] << PDFUrlLink(iPage, linkBrowse->url(), link->linkArea());
					}
				} else if (link->linkType() == Poppler::Link::Goto) {
					Poppler::LinkGoto *linkGoto = dynamic_cast<Poppler::LinkGoto*>(link);
					if (linkGoto) {
						pageJumpLinks[iPage] << PDFJumpLink(iPage,
															linkGoto->destination().pageNumber() - 1,
															QPointF(linkGoto->destination().left(), linkGoto->destination().top()),
															link->linkArea());
					}
				}
				delete link;
			}

			// Hide link bounding boxes
			foreach (Poppler::Annotation *annotation, pdfPage->annotations()) {
				if (annotation->subType() == Poppler::Annotation::ALink) {
					annotation->setFlags(annotation->flags() | Poppler::Annotation::Hidden);
				}
				delete annotation;
			}

			delete pdfPage;

			pageBBoxes_pageMutex << new QMutex(QMutex::Recursive);
		}

		chronometer.showElapsedText("load pages");
	}
	setAbortAllRendering(false);
	setUpdatesEnabled(true);
	// optimal view mode
	bool needsRelayout = false;
	if (!reloading && !pdfPageSizes.isEmpty()) {
		qreal screenPageWidth = pdfPageSizes[0].width() * physicalDpiX() / popplerDPI;
		qreal screenPageHeight = pdfPageSizes[0].height() * physicalDpiY() / popplerDPI;
		qreal aspectRatio = screenPageWidth / screenPageHeight;
		if (1/aspectRatio < 1.2) {
			needsRelayout = ui->btnSlideshow->isChecked();
			ui->btnSlideshow->setChecked(true);
		} else {
			qreal availableWidth = QApplication::desktop()->availableGeometry().width();
			if (availableWidth / screenPageWidth >= 2.0) {
				needsRelayout = ui->btnTwoPages->isChecked();
				ui->btnTwoPages->setChecked(true);
			} else {
				needsRelayout = ui->btnSinglePage->isChecked();
				ui->btnSinglePage->setChecked(true);
			}
		}
	} else {
		needsRelayout = true;
	}
	if (needsRelayout) {
		relayout();
		render();
	}
	chronometer.showElapsedText("relayout");
	updatePageNumber();

	if (!fileName.isEmpty()) {
		tryWatchFile();
	}
}

QString PDFViewer::getFileName()
{
	return fileName;
}

bool PDFViewer::isImportable()
{
	return ui->btnImport->isVisible();
}

QByteArray PDFViewer::getDocHash()
{
	return docHash;
}

bool PDFViewer::tryCreateInterDocAnchor(QByteArray docHash, int pageNumber, QPointF normalizedPos, QString reference)
{
	return ui->reportWidget->insertInterDocAnchor(docHash, pageNumber, normalizedPos, reference);
}

void PDFViewer::setFullScreen(bool value)
{
	ui->btnFullScreen->setChecked(value);
}

void PDFViewer::onShowControls(bool value)
{
	ui->widgetButtonBar->setVisible(value);
}

void PDFViewer::setControlsVisible(bool value)
{
	ui->btnShowControls->setChecked(value);
}

void PDFViewer::setImportable(bool value)
{
	ui->btnImport->setVisible(value);
	ui->frameHighlight->setVisible(!value);
	ui->frameTextNotes->setVisible(!value);
	ui->btnReport->setVisible(!value);
	ui->reportWidget->setVisible(!value);
	ui->btnDocInfo->setVisible(!value);
}

void PDFViewer::createTextNote(TextNoteAnnotation note)
{
	if (note.getPageNumber() < 0 || note.getPageNumber() >= pixPages.count())
		return;
	NoteWidget *nw = new NoteWidget(pixPages[note.getPageNumber()], docHash, note.getId());
	noteWidgets.insert(nw, true);
	nw->setWindowFlags(nw->windowFlags() | Qt::WindowStaysOnTopHint);
	nw->show();
	QPointF noteWidgetPos = normalizedPos2WidgetPos(note.getPageNumber(), note.getPos());
	nw->move(noteWidgetPos.x(), noteWidgetPos.y());
	nw->setTitle(note.getTitle());
	nw->setText(note.getRichText());
	nw->setColor(note.getColor());
	nw->setPos(note.getPos());
	nw->setMaximized(note.getMaximized());
	nw->setMaximumSize(note.getMaximumSize());
	nw->setPageNumber(note.getPageNumber());
	connect(nw, SIGNAL(deleteNoteRequested(QByteArray,int)), this, SLOT(noteDeleteRequested(QByteArray,int)));
	connect(nw, SIGNAL(titleChanged(QByteArray,int,QString)), this, SLOT(noteTitleChanged(QByteArray,int,QString)));
	connect(nw, SIGNAL(textChanged(QByteArray,int,QString)), this, SLOT(noteTextChanged(QByteArray,int,QString)));
	connect(nw, SIGNAL(posChanged(QByteArray,int,QPoint)), this, SLOT(notePosChanged(QByteArray,int,QPoint)));
	connect(nw, SIGNAL(colorChanged(QByteArray,int,QColor)), this, SLOT(noteColorChanged(QByteArray,int,QColor)));
	connect(nw, SIGNAL(maximizedChanged(QByteArray,int,bool)), this, SLOT(noteMaximizedChanged(QByteArray,int,bool)));
	connect(nw, SIGNAL(maximumSizeChanged(QByteArray,int,QSize)), this, SLOT(noteMaxSizeChanged(QByteArray,int,QSize)));
	redrawScrollBarBg();
}

void PDFViewer::deleteTextNote(int id)
{
	foreach (NoteWidget *nw, noteWidgets.keys()) {
		if (nw->getId() == id) {
			noteWidgets.remove(nw);
			nw->deleteLater();
		}
	}
	redrawScrollBarBg();
}

void PDFViewer::setReportText(QString text)
{
	ui->reportWidget->setText(text);
}

void PDFViewer::createTextHighlight(HighlightAnnotation h)
{
	int textBoxId = textBoxFromNormCoords(h.getPageNumber(), h.getBBox().center());
	if (textBoxId >= 0) {
		textBoxes[h.getPageNumber()][textBoxId].isHighlighted = true;
		textBoxes[h.getPageNumber()][textBoxId].highlightColor = h.getColor();
		pixPages[h.getPageNumber()]->repaint();
		redrawScrollBarBg();
	}
}

void PDFViewer::deleteTextHighlight(int pageNumber, QRectF rect)
{
	int textBoxId = textBoxFromNormCoords(pageNumber, rect.center());
	if (textBoxId >= 0) {
		textBoxes[pageNumber][textBoxId].isHighlighted = false;
		pixPages[pageNumber]->repaint();
		redrawScrollBarBg();
	}
}

void PDFViewer::resizeEvent(QResizeEvent *e)
{
	QWidget::resizeEvent(e);
	render();
}

void PDFViewer::showEvent(QShowEvent *e)
{
	QWidget::showEvent(e);
	render();
}

void PDFViewer::on_splitter_splitterMoved(int, int)
{
	if (ui->splitter->orientation() == Qt::Horizontal) {
		render();
	}
	int reportIndex = ui->splitter->indexOf(ui->reportWidget);
	if (reportIndex >= 0) {
		ui->btnReport->setChecked(ui->splitter->sizes().at(reportIndex) > 0);
	}
}

void PDFViewer::onReportSnapBottomRequested()
{
	ui->splitter->setOrientation(Qt::Vertical);
}

void PDFViewer::onReportSnapRightRequested()
{
	ui->splitter->setOrientation(Qt::Horizontal);
}

bool PDFViewer::eventFilter(QObject *obj, QEvent *event)
{
	if (!updatesEnabled())
		return false;
	if (event->type() == QEvent::KeyPress) {
		QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
		if (viewMode == Slideshow) {
			QStackedLayout *layout = static_cast<QStackedLayout *>(ui->viewport->layout());
			int iPage = layout->currentIndex();
			if (keyEvent->key() == Qt::Key_PageDown ||
				keyEvent->key() == Qt::Key_Right ||
				keyEvent->key() == Qt::Key_Down ||
				keyEvent->key() == Qt::Key_Space ||
				keyEvent->key() == Qt::Key_Enter ||
				keyEvent->key() == Qt::Key_Forward) {
				iPage++;
			} else if (keyEvent->key() == Qt::Key_PageUp ||
					   keyEvent->key() == Qt::Key_Left ||
					   keyEvent->key() == Qt::Key_Up ||
					   keyEvent->key() == Qt::Key_Backspace ||
					   keyEvent->key() == Qt::Key_Back) {
				iPage--;
			} else if (keyEvent->key() == Qt::Key_End) {
				iPage = pixPages.count() - 1;
			} else if (keyEvent->key() == Qt::Key_Home) {
				iPage = 0;
			}
			iPage = qMax(qMin(iPage, pixPages.count() - 1), 0);
			if (iPage != layout->currentIndex()) {
				layout->setCurrentIndex(iPage);
				updatePageNumber();
				render();
				return true;
			}
		}
	} else if (event->type() == QEvent::Wheel) {
		QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);

		if (viewMode == Slideshow) {
			QStackedLayout *layout = static_cast<QStackedLayout *>(ui->viewport->layout());
			int delta = wheelEvent->delta() > 0 ? -1 : 1;
			int iPage = qMax(qMin(layout->currentIndex() + delta, pixPages.count() - 1), 0);
			if (iPage != layout->currentIndex()) {
				layout->setCurrentIndex(iPage);
				updatePageNumber();
				render();
				return true;
			}
		}
	} else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove) {
		for (int pageNumber = 0; pageNumber < pixPages.count(); pageNumber++) {
			if (obj == pixPages[pageNumber]) {
				QMouseEvent *e = dynamic_cast<QMouseEvent *>(event);
				if (e) {
					QPointF widgetPos = pixPages[pageNumber]->mapFromGlobal(e->globalPos());
					QPointF normPos = widgetPos2NormalizedPos(pageNumber, widgetPos);
					if (ui->btnHighlightYellow->isChecked() ||
						ui->btnHighlightBlue->isChecked() ||
						ui->btnHighlightGreen->isChecked() ||
						ui->btnHighlightPink->isChecked() ||
						ui->btnHighlightRed->isChecked()) {
						QColor color = yellowHighlight;
						if (ui->btnHighlightYellow->isChecked()) {
							color = yellowHighlight;
						} else if (ui->btnHighlightGreen->isChecked()) {
							color = greenHighlight;
						} else if (ui->btnHighlightBlue->isChecked()) {
							color = blueHighlight;
						} else if (ui->btnHighlightRed->isChecked()) {
							color = redHighlight;
						} else if (ui->btnHighlightPink->isChecked()) {
							color = pinkHighlight;
						}
						int textBoxId = textBoxFromNormCoords(pageNumber, normPos);
						if (textBoxId >= 0) {
							if (e->buttons() == Qt::LeftButton) {
								emit highlightCreationRequested(this, docHash, pageNumber, textBoxes[pageNumber][textBoxId].bbox, color);
							} else if (e->buttons() == Qt::RightButton) {
								emit highlightDeletionRequested(this, docHash, pageNumber, textBoxes[pageNumber][textBoxId].bbox);
							}
						}
					} else {
						if (event->type() == QEvent::MouseButtonPress) {
							if (ui->btnAddTextNoteYellow->isChecked()) {
								ui->btnNormalCursor->setChecked(true);
								emit textNoteCreationRequested(this, docHash, pageNumber, normPos, yellowNote);
							} else if (ui->btnAddTextNoteGreen->isChecked()) {
								ui->btnNormalCursor->setChecked(true);
								emit textNoteCreationRequested(this, docHash, pageNumber, normPos, greenNote);
							} else if (ui->btnAddTextNoteBlue->isChecked()) {
								ui->btnNormalCursor->setChecked(true);
								emit textNoteCreationRequested(this, docHash, pageNumber, normPos, blueNote);
							} else if (ui->btnAddTextNotePink->isChecked()) {
								ui->btnNormalCursor->setChecked(true);
								emit textNoteCreationRequested(this, docHash, pageNumber, normPos, pinkNote);
							} else if (ui->btnAddTextNoteRed->isChecked()) {
								ui->btnNormalCursor->setChecked(true);
								emit textNoteCreationRequested(this, docHash, pageNumber, normPos, redNote);
							} else if (!(ui->btnHighlightYellow->isChecked() ||
										 ui->btnHighlightBlue->isChecked() ||
										 ui->btnHighlightGreen->isChecked() ||
										 ui->btnHighlightPink->isChecked() ||
										 ui->btnHighlightRed->isChecked())) {
								bool linkClicked = false;
								foreach (PDFUrlLink link, pageLinks[pageNumber]) {
									if (link.clickArea.contains(normPos)) {
										QDesktopServices::openUrl(link.url);
										linkClicked = true;
										break;
									}
								}
								foreach (PDFJumpLink link, pageJumpLinks[pageNumber]) {
									if (link.clickArea.contains(normPos)) {
										onNavigationRequested(link.destPageNumber, link.destPoint);
										linkClicked = true;
										break;
									}
								}
								if (!linkClicked) {
									bool ok = ui->reportWidget->insertAnchor(pageNumber, normPos);
									if (!ok) {
										emit interDocumentAnchorRequested(this, docHash, pageNumber, normPos);
									}
								}
							}
						} else if (event->type() == QEvent::MouseMove) {
							bool linkHovered = false;
							foreach (PDFUrlLink link, pageLinks[pageNumber]) {
								if (link.clickArea.contains(normPos)) {
									linkHovered = true;
									break;
								}
							}
							bool needUpdate = jumpAnchor.pageNumber >= 0;
							jumpAnchor.pageNumber = -1;
							foreach (PDFJumpLink link, pageJumpLinks[pageNumber]) {
								if (link.clickArea.contains(normPos)) {
									linkHovered = true;
									jumpAnchor.pageNumber = link.destPageNumber;
									jumpAnchor.normalizedPos = link.destPoint;
									needUpdate = true;
									break;
								}
							}
							if (needUpdate) {
								update();
							}
							if (linkHovered) {
								ui->scrollArea->setCursor(Qt::PointingHandCursor);
							} else {
								highlightCursor();
							}
						}
					}
				}
			}
		}
	} else if (event->type() == QEvent::Paint) {
		for (int pageNumber = 0; pageNumber < pixPages.count(); pageNumber++) {
			if (obj == pixPages[pageNumber]) {
				QPaintEvent *e = dynamic_cast<QPaintEvent *>(event);
				if (e) {
					bool result = true; // bypass QLabel repaint function
					if (pixPages[pageNumber]->width() > 0 && pixPages[pageNumber]->height() > 0) {
						QPainter painter(pixPages[pageNumber]);
						painter.setClipRect(e->rect());
						if (pixPages[pageNumber]->pixmap()) {
							// center horizontally
							int x = (pixPages[pageNumber]->width() - pixPages[pageNumber]->pixmap()->width())/2;
							int y;
							if (viewMode == SinglePage || viewMode == TwoPages) {
								y = 0;
							} else {
								y = (pixPages[pageNumber]->height() - pixPages[pageNumber]->pixmap()->height())/2;
							}
							painter.drawPixmap(x, y, *pixPages[pageNumber]->pixmap());
						} else {
							painter.fillRect(0, 0, pixPages[pageNumber]->width(), pixPages[pageNumber]->height(), Qt::white);
						}

						// draw text highlights
						foreach (PDFTextBox box, textBoxes[pageNumber]) {
							QRectF bboxText = normalizedRect2WidgetRect(pageNumber, box.bbox);
							bboxText.adjust(-2, -2, 2, 2);
							if (ui->btnReplaceFonts->isChecked()) {
								painter.fillRect(bboxText, Qt::white);
							}
							const bool highlightAll = false;
							if (box.isHighlighted || highlightAll) {
								QPainter::CompositionMode oldMode = painter.compositionMode();
								painter.setCompositionMode(QPainter::CompositionMode_Multiply);
								painter.fillRect(bboxText, box.highlightColor);
								painter.setCompositionMode(oldMode);
								// connect boxes
								if (box.nextId >= 0 && box.nextId < textBoxes[pageNumber].count()) {
									PDFTextBox nextBox = textBoxes[pageNumber][box.nextId];
									if (nextBox.isHighlighted) {
										QRectF nextBboxText = normalizedRect2WidgetRect(pageNumber, nextBox.bbox);
										nextBboxText.adjust(-2, -2, 2, 2);
										if (nextBboxText.left() > bboxText.right()) {
											painter.fillRect(QRectF(bboxText.topRight(), bboxText.bottomRight() + QPointF(nextBboxText.left() - bboxText.right(), 0)), box.highlightColor);
										}
									}
								}
							}
							bboxText.adjust(2, 2, -2, -2);
							if (ui->btnReplaceFonts->isChecked()) {
								QFont font = painter.font();
								font.setFamily("Bitstream Charter");
								qreal minHDiff = 1.0e9;
								int bestPS = font.pixelSize();
								for (int ps = 5; ps < 128; ps++) {
									font.setPixelSize(ps);
									QFontMetricsF fm = QFontMetricsF(font);
									QRectF textSize = fm.boundingRect(box.text);
									if (textSize.height() - bboxText.height() > 0) {
										// break;
									}
									if (qAbs(textSize.height() - bboxText.height()) < minHDiff) {
										minHDiff = qAbs(textSize.height() - bboxText.height());
										bestPS = ps;
									}
									if (textSize.height() - bboxText.height() > minHDiff) {
										break;
									}
								}
								font.setPixelSize(bestPS);
								painter.setFont(font);
								painter.drawText(bboxText, Qt::AlignBottom | Qt::TextIncludeTrailingSpaces | Qt::TextDontClip | Qt::TextSingleLine, box.text);
							}
						}

						// draw anchor targets
						QList<TextAnchor> allPageAnchors = anchors[pageNumber];
						if (jumpAnchor.pageNumber == pageNumber) {
							allPageAnchors.prepend(jumpAnchor);
						}
						foreach (TextAnchor a, allPageAnchors) {
							QPointF widgetPos = normalizedPos2WidgetPos(pageNumber, a.normalizedPos);
							if (localAnchorPixmaps.isEmpty()) {
								localAnchorPixmaps << QPixmap(":/icons/flag-blue-16.png");
								localAnchorPixmaps << QPixmap(":/icons/flag-blue-22.png");
								localAnchorPixmaps << QPixmap(":/icons/flag-blue-32.png");
								localAnchorPixmaps << QPixmap(":/icons/flag-blue-48.png");
							}
							if (interAnchorPixmaps.isEmpty()) {
								interAnchorPixmaps << QPixmap(":/icons/flag-blue-16.png");
								interAnchorPixmaps << QPixmap(":/icons/flag-blue-22.png");
								interAnchorPixmaps << QPixmap(":/icons/flag-blue-32.png");
								interAnchorPixmaps << QPixmap(":/icons/flag-blue-48.png");
							}
							bool local = a.docHash.isEmpty() || a.docHash == docHash;
							int optimalWidth = pixPages[pageNumber]->width() / 30;
							int bestIndex = 0;
							if (local) {
								for (int i = 0; i < localAnchorPixmaps.count(); i++) {
									if (localAnchorPixmaps[i].width() <= optimalWidth) {
										bestIndex = i;
									}
								}
								painter.drawPixmap(widgetPos, localAnchorPixmaps[bestIndex]);
							}
						}

						painter.end();
					}
					return result;
				}
			}
		}
	}

	bool result = QObject::eventFilter(obj, event);
	return result;
}

QSizeF PDFViewer::getAvailablePageSize()
{
	QSizeF availableSize = QSizeF(ui->scrollArea->viewport()->width() - ui->scrollArea->verticalScrollBar()->minimumSizeHint().width()
								  - style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing),
								  ui->scrollArea->viewport()->height() - ui->scrollArea->horizontalScrollBar()->minimumSizeHint().height()
								  - style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing));
	if (viewMode == SinglePage) {
		availableSize.rwidth() -= ui->viewport->layout()->contentsMargins().left() + ui->viewport->layout()->contentsMargins().right();
		availableSize.rheight() -= ui->viewport->layout()->contentsMargins().top() + ui->viewport->layout()->contentsMargins().bottom();
	} else if (viewMode == TwoPages) {
		availableSize.rwidth() -= ui->viewport->layout()->contentsMargins().left() + ui->viewport->layout()->contentsMargins().right() +
				ui->viewport->layout()->spacing();
		availableSize.rheight() -= ui->viewport->layout()->contentsMargins().top() + ui->viewport->layout()->contentsMargins().bottom();
	} else if (viewMode == Slideshow) {
		availableSize.rwidth() -= ui->viewport->layout()->contentsMargins().left() + ui->viewport->layout()->contentsMargins().right();
		availableSize.rheight() -= ui->viewport->layout()->contentsMargins().top() + ui->viewport->layout()->contentsMargins().bottom();
	}

	return availableSize;
}

void PDFViewer::render()
{
	if (getAbortAllRendering())
		return;
	QSizeF availableSize = getAvailablePageSize();
	QDesktopWidget desktop;
	if (availableSize.width() > desktop.width() ||
		availableSize.height() > desktop.height()) {
		availableSize = desktop.size();
	}

	int totalHeight = 0;
	QList<int> pageTops;
	QList<QSize> pageSizes;

	totalHeight += ui->viewport->layout()->contentsMargins().top();
	for (int iPage = 0; iPage < pixPages.count(); iPage++) {
		// dummy crop
		QRectF pageBBox = getPageBBox(iPage, true);

		QSizeF pageSize = pdfPageSizes[iPage];

		QRectF croppedPageRect = QRectF(pageSize.width() * pageBBox.left(),
										pageSize.height() * pageBBox.top(),
										pageSize.width() * pageBBox.width(),
										pageSize.height() * pageBBox.height());
		QSizeF pixSize;
		if (viewMode == SinglePage) {
			pixSize.setWidth(availableSize.width());
			pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
		} else if (viewMode == TwoPages) {
			pixSize.setWidth(availableSize.width()/2.0);
			pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
		} else if (viewMode == Slideshow) {
			pixSize.setHeight(availableSize.height());
			pixSize.setWidth(croppedPageRect.width() / croppedPageRect.height() * pixSize.height());
			if (pixSize.width() > availableSize.width()) {
				pixSize.setWidth(availableSize.width());
				pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
			}
		}

		QSizeF fullPixSize;
		fullPixSize.setWidth(pixSize.width() / pageBBox.width());
		fullPixSize.setHeight(pixSize.height() / pageBBox.height());

		QSize imageSize(fullPixSize.width() * pageBBox.width(), fullPixSize.height() * pageBBox.height());

		if (viewMode == SinglePage) {
			totalHeight += iPage > 0 ? ui->viewport->layout()->spacing() : 0;
			pageTops << totalHeight;
			pageSizes << imageSize;
			totalHeight += imageSize.height();
		} else if (viewMode == TwoPages) {
			totalHeight += iPage > 0 && iPage % 2 == 0 ? ui->viewport->layout()->spacing() : 0;
			pageTops << totalHeight;
			pageSizes << imageSize;
			totalHeight += (iPage % 2 == 1) ? qMax(imageSize.height(), pageSizes[iPage-1].height()) : 0;
		} else if (viewMode == Slideshow) {
			QStackedLayout *layout = static_cast<QStackedLayout *>(ui->viewport->layout());
			pageTops << totalHeight;
			pageSizes << imageSize;
			totalHeight += (layout->currentIndex() == iPage) ? imageSize.height() : 0;
		}
	}
	totalHeight += ui->viewport->layout()->contentsMargins().bottom();

	int yVisibleStart = ui->scrollArea->verticalScrollBar()->value();
	int yVisibleEnd = yVisibleStart + availableSize.height() - 1;

	QList<bool> pageVisible;
	for (int iPage = 0; iPage < pixPages.count(); iPage++) {
		pageVisible << true;
		if (viewMode == SinglePage) {
			pageVisible[iPage] = !(pageTops[iPage] + pageSizes[iPage].height() < yVisibleStart || yVisibleEnd < pageTops[iPage]);
		} else if (viewMode == TwoPages) {
			int evenPage = iPage & (INT_MAX-1);
			int oddPage = iPage | 1;
			if (evenPage >= pixPages.count())
				evenPage = iPage;
			if (oddPage >= pixPages.count())
				oddPage = iPage;
			pageVisible[iPage] = !(pageTops[iPage] + qMax(pageSizes[oddPage].height(), pageSizes[evenPage].height()) < yVisibleStart || yVisibleEnd < pageTops[iPage]);
		} else if (viewMode == Slideshow) {
			QStackedLayout *layout = static_cast<QStackedLayout *>(ui->viewport->layout());
			pageVisible[iPage] = layout->currentIndex() == iPage;
		}
	}

	QList<bool> pageShouldRender = pageVisible;
	for (int iPage = 0; iPage < pixPages.count(); iPage++) {
		int actualPreRenderDistance;
		if (viewMode == TwoPages) {
			actualPreRenderDistance = 2 * preRenderDistance;
		} else {
			actualPreRenderDistance = preRenderDistance;
		}
		for (int delta = -actualPreRenderDistance; delta <= actualPreRenderDistance; delta++) {
			int iNeighbor = iPage + delta;
			if (0 <= iNeighbor && iNeighbor < pageVisible.count()) {
				pageShouldRender[iPage] = pageShouldRender[iPage] || pageVisible[iNeighbor];
			}
		}
	}
	{
		QMutexLocker lockerRendererPageMask(rendererPageMask_mutex); Q_UNUSED(lockerRendererPageMask);
		rendererPageMask = pageShouldRender;
	}

	for (int iPage = 0; iPage < pixPages.count(); iPage++) {
		if (!pageShouldRender[iPage]) {
			pixPages[iPage]->setPixmap(QPixmap());
			pixPages[iPage]->setMinimumSize(pageSizes[iPage]);
			pixPages[iPage]->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
			continue;
		}

		pixPages[iPage]->setMinimumSize(pageSizes[iPage]);
		pixPages[iPage]->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

		QRectF pageBBox;
		if (crop) {
			pageBBox = getPageBBox(iPage, false);
			if (pageBBox.isNull()) {
				startRenderAsync1(iPage);
				continue;
			}
		} else {
			pageBBox = QRectF(0, 0, 1, 1);
		}

		onRenderSync1(iPage);
	}
	redrawScrollBarBg();
	firstRenderReady = true;
}

QRectF PDFViewer::getPageBBox(int iPage, bool useDefault, bool *returnedDefault)
{
	if (getAbortAllRendering())
		return QRectF(0, 0, 1, 1);
	QMutex *pageMutex;
	{
		QMutexLocker locker(pageBBoxes_mutex); Q_UNUSED(locker);
		pageMutex = pageBBoxes_pageMutex[iPage];

		if (useDefault) {
			if (pageMutex->tryLock()) {
				QRectF result;
				if (crop) {
					if (pageBBoxes[iPage].isNull()) {
						result = QRectF(0, 0, 1, 1);
						if (returnedDefault != NULL)
							*returnedDefault = true;
					} else {
						result = pageBBoxes[iPage];
						if (returnedDefault != NULL)
							*returnedDefault = false;
					}
				} else {
					result = QRectF(0, 0, 1, 1);
					if (returnedDefault != NULL)
						*returnedDefault = true;
				}
				pageMutex->unlock();
				return result;
			} else {
				if (returnedDefault != NULL)
					*returnedDefault = true;
				return QRectF(0, 0, 1, 1);
			}
		} else {
			QMutexLocker lockerPage(pageMutex); Q_UNUSED(lockerPage);
			if (returnedDefault != NULL)
				*returnedDefault = false;
			return pageBBoxes[iPage];
		}
	}
}

void PDFViewer::startRenderAsync1(int iPage)
{
	runningJobs << QtConcurrent::run(this, &PDFViewer::renderAsync1, iPage);
}

// May run in a non-GUI thread
void PDFViewer::renderAsync1(int iPage)
{
	//check if the job is still needed
	if (getAbortAllRendering())
		return;
	{
		QMutexLocker lockerRendererPageMask(rendererPageMask_mutex); Q_UNUSED(lockerRendererPageMask);
		if (iPage >= rendererPageMask.count() || !rendererPageMask[iPage])
			return;
	}

	QMutex *pageMutex;
	{
		QMutexLocker locker(pageBBoxes_mutex); Q_UNUSED(locker);
		pageMutex = pageBBoxes_pageMutex[iPage];
		pageMutex->lock();
	}
	// check if the result is cached
	QSizeF resolution(72.0, 72.0);
	QRect cropRect(-1, -1, -1, -1);
	QImage image = getImageFromCache(iPage, cropRect, resolution);
	if (image.isNull()) {
		{
			QMutexLocker lockerDoc(documentMutex); Q_UNUSED(lockerDoc);
			Poppler::Page* pdfPage = document->page(iPage);  // Document starts at page 0
			if (!pdfPage) {
				pageMutex->unlock();
				return;
			}
			if (getAbortAllRendering()) {
				pageMutex->unlock();
				return;
			}
			image = pdfPage->renderToImage(resolution.width(), resolution.height(), cropRect.left(), cropRect.top(), cropRect.width(), cropRect.height());
			delete pdfPage;
		}
		addImageToCache(iPage, cropRect, resolution, image);
	} else {
		qDebug() << "Loading from cache:" << iPage << cropRect << resolution;
	}
	pageBBoxes[iPage] = tightBoundingRect(image, minBorder, maxCropFraction);
	pageMutex->unlock();
	if (getAbortAllRendering()) {
		return;
	}
	emit renderSync1(iPage);
}

void PDFViewer::onRenderSync1(int iPage)
{
	if (getAbortAllRendering())
		return;
	{
		QMutexLocker lockerDoc(documentMutex); Q_UNUSED(lockerDoc);
		if (!document || iPage >= pixPages.count())
			return;
	}
	QSizeF availableSize = getAvailablePageSize();

	QRectF pageBBox;
	if (crop) {
		pageBBox = getPageBBox(iPage, true);
		if (pageBBox.isNull()) {
			startRenderAsync1(iPage);
			return;
		}
	} else {
		pageBBox = QRectF(0, 0, 1, 1);
	}

	// render page
	QSizeF pageSize = pdfPageSizes[iPage];
	// croppedPageRect is esentially pageBBox measured in some poppler-specific units
	QRectF croppedPageRect = QRectF(pageSize.width() * pageBBox.left(),
									pageSize.height() * pageBBox.top(),
									pageSize.width() * pageBBox.width(),
									pageSize.height() * pageBBox.height());
	// pixSize is the size of the pixmap we want to display
	// this is not the same as croppedPageRect.size() because we need to pad the image to match the window size
	QSizeF pixSize;
	if (viewMode == SinglePage) {
		pixSize.setWidth(availableSize.width());
		pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
	} else if (viewMode == TwoPages) {
		pixSize.setWidth(availableSize.width()/2.0);
		pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
	} else if (viewMode == Slideshow) {
		pixSize.setHeight(availableSize.height());
		pixSize.setWidth(croppedPageRect.width() / croppedPageRect.height() * pixSize.height());
		if (pixSize.width() > availableSize.width()) {
			pixSize.setWidth(availableSize.width());
			pixSize.setHeight(croppedPageRect.height() / croppedPageRect.width() * pixSize.width());
		}
	}

	QSizeF fullPixSize;
	fullPixSize.setWidth(pixSize.width() / pageBBox.width());
	fullPixSize.setHeight(pixSize.height() / pageBBox.height());
	QSize imageSize(fullPixSize.width() * pageBBox.width(), fullPixSize.height() * pageBBox.height());

	if (pixPages[iPage]->pixmap() && pixPages[iPage]->pixmap()->size() == imageSize)
		return;

	// check cache
	QSizeF resolution(popplerDPI * fullPixSize.width()/pageSize.width(), popplerDPI * fullPixSize.height()/pageSize.height());
	QRect cropRect(fullPixSize.width() * pageBBox.left(), fullPixSize.height() * pageBBox.top(),
				   fullPixSize.width() * pageBBox.width(), fullPixSize.height() * pageBBox.height());
	QImage image = getImageFromCache(iPage, cropRect, resolution);
	if (!image.isNull()) {
		pixPages[iPage]->setPixmap(QPixmap::fromImage(image));
		return;
	}

	// render a white image for now, schedule an asynchronous rendering
	pixPages[iPage]->setPixmap(QPixmap());
	redrawScrollBarBg();

	startRenderAsync2(iPage, resolution, cropRect);
	clearFinishedJobs();

	// reposition annotations
	foreach (QObject *w, pixPages[iPage]->children()) {
		NoteWidget *nw = dynamic_cast<NoteWidget*>(w);
		if (nw) {
			nw->move(normalizedPos2WidgetPos(iPage, nw->getPos()).toPoint());
		}
	}
}

void PDFViewer::startRenderAsync2(int iPage, QSizeF resolution, QRect rect)
{
	runningJobs << QtConcurrent::run(this, &PDFViewer::renderAsync2, iPage, resolution, rect);
}

// May run in a non-GUI thread
void PDFViewer::renderAsync2(int iPage, QSizeF resolution, QRect cropRect)
{
	//check if the job is still needed
	if (getAbortAllRendering())
		return;
	{
		QMutexLocker lockerRendererPageMask(rendererPageMask_mutex); Q_UNUSED(lockerRendererPageMask);
		if (iPage >= rendererPageMask.count() || !rendererPageMask[iPage])
			return;
	}

	QImage image = getImageFromCache(iPage, cropRect, resolution);
	if (image.isNull()) {
		{
			QMutexLocker lockerDoc(documentMutex); Q_UNUSED(lockerDoc);
			if (!document)
				return;
			Poppler::Page* pdfPage = document->page(iPage);  // Document starts at page 0
			if (!pdfPage) {
				return;
			}
			if (getAbortAllRendering())
				return;
			image = pdfPage->renderToImage(resolution.width(), resolution.height(), cropRect.left(), cropRect.top(), cropRect.width(), cropRect.height());
			delete pdfPage;
		}
		addImageToCache(iPage, cropRect, resolution, image);
	} else {
		qDebug() << "Loading from cache:" << iPage << cropRect << resolution;
	}
	if (getAbortAllRendering())
		return;
	emit renderSync2(iPage, image);
}

void PDFViewer::onRenderSync2(int iPage, QImage image)
{
	QLabel *pixPage = pixPages[iPage];
	// ignore out of order events
	if (pixPage->pixmap() && pixPage->pixmap()->size() != image.size()) {
		return;
	}
	pixPage->setPixmap(QPixmap::fromImage(image));
	clearFinishedJobs();

	// reposition annotations
	foreach (QObject *w, pixPages[iPage]->children()) {
		NoteWidget *nw = dynamic_cast<NoteWidget*>(w);
		if (nw) {
			nw->move(normalizedPos2WidgetPos(iPage, nw->getPos()).toPoint());
		}
	}
}

void PDFViewer::relayout()
{
	if (ui->viewport->layout()) {
		delete ui->viewport->layout();
	}

	if (viewMode == SinglePage) {
		QVBoxLayout *layout = new QVBoxLayout(ui->viewport);
		ui->viewport->setLayout(layout);
		ui->viewport->layout()->setContentsMargins(shadowSize, shadowSize, shadowSize, shadowSize);
		ui->viewport->layout()->setSpacing(shadowSize);
		ui->viewport->layout()->setAlignment(Qt::AlignCenter);

		for (int iPage = 0; iPage < pixPages.count(); iPage++) {
			layout->addWidget(pixPages[iPage]);
			pixPages[iPage]->setVisible(true);
		}
	} else if (viewMode == TwoPages) {
		QGridLayout *layout = new QGridLayout(ui->viewport);
		ui->viewport->setLayout(layout);
		ui->viewport->layout()->setContentsMargins(shadowSize, shadowSize, shadowSize, shadowSize);
		ui->viewport->layout()->setSpacing(shadowSize);
		ui->viewport->layout()->setAlignment(Qt::AlignHCenter | Qt::AlignTop);

		for (int iPage = 0; iPage < pixPages.count(); iPage++) {
			layout->addWidget(pixPages[iPage], iPage/2, iPage%2);
			pixPages[iPage]->setVisible(true);
		}
	} else if (viewMode == Slideshow) {
		QStackedLayout *layout = new QStackedLayout(ui->viewport);
		ui->viewport->setLayout(layout);
		ui->viewport->layout()->setContentsMargins(shadowSize, shadowSize, shadowSize, shadowSize);
		ui->viewport->layout()->setSpacing(shadowSize);
		ui->viewport->layout()->setAlignment(Qt::AlignCenter);

		for (int iPage = 0; iPage < pixPages.count(); iPage++) {
			layout->addWidget(pixPages[iPage]);
		}
	}
}

void PDFViewer::mousePoll()
{
	if (!ui->btnShowControls->isChecked()) {
		if (QCursor::pos().y() <= ui->scrollArea->viewport()->mapToGlobal(QPoint(2, 2)).y() ||
			QCursor::pos().x() <= ui->scrollArea->viewport()->mapToGlobal(QPoint(2, 2)).x()) {
			emit showControls(true);
		} else {
			if (QCursor::pos().y() >= ui->scrollArea->viewport()->mapToGlobal(QPoint(0, 0)).y() &&
				QCursor::pos().x() >= ui->scrollArea->viewport()->mapToGlobal(QPoint(0, 0)).x()) {
				emit showControls(false);
			}
		}
	}
}

void PDFViewer::noteDeleteRequested(QByteArray docHash, int id)
{
	emit textNoteDeleteRequested(this, docHash, id);
}

void PDFViewer::noteTitleChanged(QByteArray docHash, int id, QString title)
{
	emit textNoteTitleChanged(this, docHash, id, title);
}

void PDFViewer::noteTextChanged(QByteArray docHash, int id, QString text)
{
	emit textNoteTextChanged(this, docHash, id, text);
}

void PDFViewer::notePosChanged(QByteArray docHash, int id, QPoint pos)
{
	NoteWidget *nw = dynamic_cast<NoteWidget*>(QObject::sender());
	if (nw) {
		QPointF normPos = widgetPos2NormalizedPos(nw->getPageNumber(), pos);
		emit textNotePosChanged(this, docHash, id, normPos);
		redrawScrollBarBg();
	}
}

void PDFViewer::noteColorChanged(QByteArray docHash, int id, QColor color)
{
	emit textNoteColorChanged(this, docHash, id, color);
	redrawScrollBarBg();
}

void PDFViewer::noteMaximizedChanged(QByteArray docHash, int id, bool value)
{
	emit textNoteMaximizedChanged(this, docHash, id, value);
}

void PDFViewer::noteMaxSizeChanged(QByteArray docHash, int id, QSize maxSize)
{
	emit textNoteMaxSizeChanged(this, docHash, id, maxSize);
}

void PDFViewer::onReportTextChanged(QString text)
{
	emit reportTextChanged(this, docHash, text);
	QList<int> pagesToRefresh;
	foreach (int pageNumber, anchors.uniqueKeys()) {
		if (pageNumber >= 0 && pageNumber < pixPages.count()) {
			pagesToRefresh << pageNumber;
		}
	}

	anchors.clear();
	foreach (TextAnchor a, ui->reportWidget->anchors()) {
		if (a.pageNumber >= 0 && a.pageNumber < pixPages.count()) {
			anchors[a.pageNumber].append(a);
		}
	}
	foreach (int pageNumber, anchors.uniqueKeys()) {
		if (pageNumber >= 0 && pageNumber < pixPages.count()) {
			pagesToRefresh << pageNumber;
		}
	}
	foreach (int pageNumber, pagesToRefresh) {
		pixPages[pageNumber]->repaint();
	}
}

QPointF PDFViewer::normalizedPos2WidgetPos(int pageNumber, QPointF normPos)
{
	if (pageNumber > pixPages.count())
		return normPos;
	QRectF pageBBox;
	if (crop) {
		pageBBox = getPageBBox(pageNumber, false);
		if (pageBBox.isNull()) {
			pageBBox = QRectF(0, 0, 1, 1);
		}
	} else {
		pageBBox = QRectF(0, 0, 1, 1);
	}
	QPointF realPos = normPos;
	// crop
	realPos.rx() = (realPos.rx() - pageBBox.left()) / pageBBox.width();
	realPos.ry() = (realPos.ry() - pageBBox.top()) / pageBBox.height();
	// scale to widget coords
	if (pixPages[pageNumber]->pixmap()) {
		// pixmap offset
		int x = (pixPages[pageNumber]->width() - pixPages[pageNumber]->pixmap()->width())/2;
		int y = 0;
		if (viewMode == SinglePage || viewMode == TwoPages) {
			y = 0;
		} else if (viewMode == Slideshow) {
			y = (pixPages[pageNumber]->height() - pixPages[pageNumber]->pixmap()->height())/2;
		}
		realPos.rx() *= qreal(pixPages[pageNumber]->pixmap()->width());
		realPos.ry() *= qreal(pixPages[pageNumber]->pixmap()->height());
		realPos.rx() = qMin(qMax(realPos.rx(), 0.0), (qreal)(pixPages[pageNumber]->pixmap()->width()));
		realPos.ry() = qMin(qMax(realPos.ry(), 0.0), (qreal)(pixPages[pageNumber]->pixmap()->height()));
		// finally rel to label
		realPos.rx() += x;
		realPos.ry() += y;
	} else {
		realPos.rx() *= qreal(pixPages[pageNumber]->width());
		realPos.ry() *= qreal(pixPages[pageNumber]->height());
	}
	return realPos;
}

QPointF PDFViewer::widgetPos2NormalizedPos(int pageNumber, QPointF widgetPos)
{
	if (pageNumber > pixPages.count())
		return widgetPos;
	QRectF pageBBox;
	if (crop) {
		pageBBox = getPageBBox(pageNumber, false);
		if (pageBBox.isNull()) {
			pageBBox = QRectF(0, 0, 1, 1);
		}
	} else {
		pageBBox = QRectF(0, 0, 1, 1);
	}
	QPointF realPos = widgetPos;
	// normalize to cropped area
	if (pixPages[pageNumber]->pixmap()) {
		// pixmap offset
		int x = (pixPages[pageNumber]->width() - pixPages[pageNumber]->pixmap()->width())/2;
		int y;
		if (viewMode == SinglePage || viewMode == TwoPages) {
			y = 0;
		} else {
			y = (pixPages[pageNumber]->height() - pixPages[pageNumber]->pixmap()->height())/2;
		}
		// rel to pixmap
		realPos.rx() -= x;
		realPos.ry() -= y;
		realPos.rx() = qMin(qMax(realPos.rx(), 0.0), (qreal)(pixPages[pageNumber]->pixmap()->width()));
		realPos.ry() = qMin(qMax(realPos.ry(), 0.0), (qreal)(pixPages[pageNumber]->pixmap()->height()));
		// normalize
		realPos.rx() /= qreal(pixPages[pageNumber]->pixmap()->width());
		realPos.ry() /= qreal(pixPages[pageNumber]->pixmap()->height());
	} else {
		realPos.rx() /= qreal(pixPages[pageNumber]->width());
		realPos.ry() /= qreal(pixPages[pageNumber]->height());
	}
	// uncrop
	realPos.rx() = realPos.rx() * pageBBox.width() + pageBBox.left();
	realPos.ry() = realPos.ry() * pageBBox.height() + pageBBox.top();
	return realPos;
}

QRectF PDFViewer::normalizedRect2WidgetRect(int pageNumber, QRectF normRect)
{
	return QRectF(normalizedPos2WidgetPos(pageNumber, normRect.topLeft()), normalizedPos2WidgetPos(pageNumber, normRect.bottomRight()));
}

QRectF PDFViewer::widgetRect2NormalizedRect(int pageNumber, QRectF widgetRect)
{
	return QRectF(widgetPos2NormalizedPos(pageNumber, widgetRect.topLeft()), widgetPos2NormalizedPos(pageNumber, widgetRect.bottomRight()));
}

int PDFViewer::textBoxFromNormCoords(int pageNumber, QPointF normPos)
{
	if (!textBoxes.contains(pageNumber))
		return -1;
	for (int i = 0; i < textBoxes[pageNumber].count(); i++) {
		if (textBoxes[pageNumber][i].bbox.contains(normPos)) {
			return i;
		}
	}
	return -1;
}

void PDFViewer::onNavigationRequested(int pageNumber, QPointF normPos)
{
	if (pageNumber < 0 || pageNumber >= pixPages.count())
		return;
	QPointF pos = pixPages[pageNumber]->pos() + normalizedPos2WidgetPos(pageNumber, normPos);
	if (ui->scrollArea->verticalScrollBar()) {
		ui->scrollArea->verticalScrollBar()->setValue(qMax(0, (int)(pos.y() - 50)));
	}
}

void PDFViewer::setViewOffset(qreal offsetNormalized)
{
	if (ui->scrollArea->verticalScrollBar()->minimum() == ui->scrollArea->verticalScrollBar()->maximum() || ui->scrollArea->verticalScrollBar()->maximum() < minimumSizeHint().height()) {
		navigationPending = true;
		navigationOffset = offsetNormalized;
	} else {
		ui->scrollArea->verticalScrollBar()->setValue(offsetNormalized * ui->scrollArea->verticalScrollBar()->maximum());
	}
}

void PDFViewer::setSuggestedFileName(QString saveName)
{
	suggestedFileName = saveName;
}

void PDFViewer::showDocInfo(QString docInfo)
{
	QTextEdit* textEdit = new QTextEdit(NULL);
	textEdit->setAttribute(Qt::WA_DeleteOnClose);
	textEdit->show();
	textEdit->setWindowTitle("Document info");
	textEdit->setText(docInfo);
	textEdit->resize(600, 400);
}

void PDFViewer::onNavigationRequested(QByteArray docHash, int pageNumber, QPointF normPos)
{
	if (docHash == this->docHash) {
		onNavigationRequested(pageNumber, normPos);
	} else {
		emit navigationRequested(docHash, pageNumber, normPos);
	}
}

void PDFViewer::setAbortAllRendering(bool value)
{
	QMutexLocker locker(signalingMutex); Q_UNUSED(locker);
	abortAllRendering = value;
}

bool PDFViewer::getAbortAllRendering()
{
	QMutexLocker locker(signalingMutex); Q_UNUSED(locker);
	return abortAllRendering;
}

QImage PDFViewer::getImageFromCache(int pageNumber, QRect cropRect, QSizeF resolution)
{
	QMutexLocker locker(renderCacheMutex); Q_UNUSED(locker);
	renderCache[RenderCacheKey(pageNumber, cropRect, resolution)].second = Chronometer::currentTimeMs();
	return renderCache[RenderCacheKey(pageNumber, cropRect, resolution)].first;
}

void PDFViewer::addImageToCache(int pageNumber, QRect cropRect, QSizeF resolution, QImage image)
{
	qDebug() << "Saving to cache   :" << pageNumber << cropRect << resolution;
	QMutexLocker locker(renderCacheMutex); Q_UNUSED(locker);
	renderCache[RenderCacheKey(pageNumber, cropRect, resolution)].second = Chronometer::currentTimeMs();
	renderCache[RenderCacheKey(pageNumber, cropRect, resolution)].first = image;

	quint64 cacheSize = 0;
	QMap<qint64, QPair<RenderCacheKey, int> > cacheBreakdown;
	foreach (RenderCacheKey key, renderCache.keys()) {
		int size = renderCache[key].first.size().width() * renderCache[key].first.size().height() * 4;
		int timestamp = renderCache[key].second;
		cacheSize += size;
		cacheBreakdown.insertMulti(timestamp, QPair<RenderCacheKey, int>(key, size));
	}
	while (cacheSize > 250 * 1000 * 1000 && !cacheBreakdown.isEmpty()) {
		qint64 timestamp = cacheBreakdown.begin().key();
		QList<QPair<RenderCacheKey, int> > values = cacheBreakdown.values(timestamp);
		cacheBreakdown.remove(timestamp);
		for (int i = 0; i < values.count(); i++) {
			RenderCacheKey key = values[i].first;
			int size = values[i].second;
			renderCache.remove(key);
			cacheSize -= size;
		}
	}
	emit pageCacheChanged(this, docHash);
}

void PDFViewer::scrollValueChanged(int)
{
	if (ui->scrollArea->verticalScrollBar()->minimum() == ui->scrollArea->verticalScrollBar()->maximum() || ui->scrollArea->verticalScrollBar()->maximum() < minimumSizeHint().height())
		return;
	emit viewOffsetCHanged(docHash, ui->scrollArea->verticalScrollBar()->value() / qreal(ui->scrollArea->verticalScrollBar()->maximum()));
	updatePageNumber();
	render();
}

void PDFViewer::scrollRangeChanged(int min, int max)
{
	if (min == max || max < minimumSizeHint().height())
		return;
	if (navigationPending) {
		if (firstRenderReady) {
			navigationPending = false;
			ui->scrollArea->verticalScrollBar()->setValue(navigationOffset * ui->scrollArea->verticalScrollBar()->maximum());
		}
	}
	if (oldRange.first != oldRange.second) {
		qreal oldp = ui->scrollArea->verticalScrollBar()->value() / qreal(oldRange.second);
		oldRange = QPair<int, int>(min, max);
		ui->scrollArea->verticalScrollBar()->setValue(oldp * max);
	}
	oldRange = QPair<int, int>(min, max);
	emit viewOffsetCHanged(docHash, ui->scrollArea->verticalScrollBar()->value() / qreal(ui->scrollArea->verticalScrollBar()->maximum()));
	updatePageNumber();
	render();
}

void PDFViewer::on_btnSinglePage_toggled(bool checked)
{
	if (checked) {
		viewMode = SinglePage;
		relayout();
		render();
	}
}

void PDFViewer::on_btnSlideshow_toggled(bool checked)
{
	if (checked) {
		viewMode = Slideshow;
		relayout();
		render();
	}
}

void PDFViewer::on_btnTwoPages_toggled(bool checked)
{
	if (checked) {
		viewMode = TwoPages;
		relayout();
		render();
	}
}

void PDFViewer::on_btnMinBorder_toggled(bool checked)
{
	crop = checked;
	render();
}

void PDFViewer::on_btnFullScreen_toggled(bool checked)
{
	emit fullScreenToggled(checked);
}

void PDFViewer::on_btnShowControls_toggled(bool checked)
{
	emit controlsVisibleToggled(checked);
}

void PDFViewer::onHideJumpTarget()
{
	jumpAnchor.pageNumber = -1;
	update();
}

QHash<RenderCacheKey, QPair<QImage, qint64> > PDFViewer::getRenderCache()
{
	QMutexLocker locker(renderCacheMutex); Q_UNUSED(locker);
	return renderCache;
}

void PDFViewer::clearFinishedJobs()
{
	for (int i = 0; i < runningJobs.count(); i++) {
		if (runningJobs[i].isFinished()) {
			runningJobs.removeAt(i);
			i--;
		}
	}
}

void PDFViewer::on_btnOpenOutside_clicked()
{
	QDesktopServices::openUrl(fileName);
}

void PDFViewer::on_btnImport_clicked()
{
	emit importClicked(fileName);
}


void PDFViewer::on_btnReplaceFonts_toggled(bool)
{
	foreach (QLabel *pixPage, pixPages) {
		pixPage->repaint();
	}
}

void PDFViewer::highlightCursor()
{
	if (ui->btnHighlightYellow->isChecked() || ui->btnHighlightRed->isChecked() || ui->btnHighlightBlue->isChecked() ||
			ui->btnHighlightPink->isChecked() || ui->btnHighlightGreen->isChecked()) {
		ui->scrollArea->setCursor(Qt::IBeamCursor);
	} else {
		ui->scrollArea->setCursor(Qt::ArrowCursor);
	}
}

void PDFViewer::on_btnHighlightYellow_toggled(bool)
{
	highlightCursor();
}

void PDFViewer::on_btnHighlightGreen_toggled(bool)
{
	highlightCursor();
}

void PDFViewer::on_btnHighlightRed_toggled(bool)
{
	highlightCursor();
}

void PDFViewer::on_btnHighlightPink_toggled(bool)
{
	highlightCursor();
}

void PDFViewer::on_btnHighlightBlue_toggled(bool)
{
	highlightCursor();
}

void PDFViewer::on_btnSaveOutside_clicked()
{
	QString saveName = QFileDialog::getSaveFileName(this, "Save file as...", QDir::homePath() + "/" + suggestedFileName, "PDF Files (*.pdf)");
	if (saveName.isEmpty())
		return;
	if (!saveName.endsWith(".pdf", Qt::CaseInsensitive))
		saveName += ".pdf";
	if (!QFile::copy(fileName, saveName)) {
		QMessageBox::critical(this, "Error while saving file", QString("Could not copy %1 to %2.\nReason: %3").arg(fileName).arg(saveName).arg(QFile(saveName).exists() ? "a file with the specified destination name already exists." : "unknown."), QMessageBox::Ok);
	}
}

void PDFViewer::on_btnReport_toggled(bool)
{
	int reportIndex = ui->splitter->indexOf(ui->reportWidget);
	int viewerIndex = ui->splitter->indexOf(ui->scrollArea);
	if (reportIndex >= 0 && viewerIndex >= 0) {
		QList<int> sizes = ui->splitter->sizes();
		QSettings settings;
		int oldViewerWidth = settings.value("PDFViewer/viewerWidth", QVariant::fromValue(0)).toInt();
		int oldReportWidth = settings.value("PDFViewer/reportWidth", QVariant::fromValue(0)).toInt();
		if (ui->btnReport->isChecked()) {
			if (oldViewerWidth <= 0 || oldReportWidth <= 0) {
				sizes[viewerIndex] = ui->splitter->orientation() == Qt::Horizontal ? (1 + width()/2) : sizes[viewerIndex];
				sizes[reportIndex] = ui->splitter->orientation() == Qt::Horizontal ? (1 + width()/2) : ui->reportWidget->sizeHint().height();
			} else {
				sizes[viewerIndex] = oldViewerWidth;
				sizes[reportIndex] = oldReportWidth;
			}
		} else {
			if (ui->splitter->orientation() == Qt::Horizontal) {
				settings.setValue("PDFViewer/viewerWidth", QVariant::fromValue(sizes[viewerIndex]));
				settings.setValue("PDFViewer/reportWidth", QVariant::fromValue(sizes[reportIndex]));
			}
			sizes[reportIndex] = 0;
		}
		ui->splitter->setSizes(sizes);
		if (ui->splitter->orientation() == Qt::Horizontal) {
			render();
		}
	}
}

void setupPrinter(QPrinter &printer, QString fileName, int resolution, QSizeF paperSize)
{
	printer.setOutputFileName(fileName);
	printer.setResolution(resolution);
	printer.setOrientation(QPrinter::Portrait);
	printer.setPageMargins(0, 0, 0, 0, QPrinter::DevicePixel);
	printer.setFullPage(true);
    //printer.setCopyCount(1);
	printer.setColorMode(QPrinter::Color);
	printer.setPaperSize(paperSize, QPrinter::DevicePixel);
}

void PDFViewer::on_btnSaveOutsideAnnotated_clicked()
{
	QString saveName = QFileDialog::getSaveFileName(this, "Save file as...", QDir::homePath() + "/" + suggestedFileName, "PDF Files (*.pdf)");
	if (saveName.isEmpty())
		return;
	if (!saveName.endsWith(".pdf", Qt::CaseInsensitive))
		saveName += ".pdf";

	bool scalable = QMessageBox::question(this, "Format",
										  "Would you like to use the scalable format (new, experimental) or rasterized?",
										  "Scalable", "Rasterized") == 0;

	int res;
	if (!scalable) {
		bool ok;
		res = QInputDialog::getInt(this, "Resolution", "The document will be rasterized. Please enter the resolution (DPI):", 300, 10, 1200, 50, &ok);
		if (!ok)
			return;
	} else {
		res = 600;
	}

	Poppler::Document* document = Poppler::Document::load(fileName);
	if (!document || document->isLocked()) {
		delete document;
		document = NULL;
		QMessageBox::critical(this, "Save failed", "Could not open document.", QMessageBox::Ok);
		return;
	}

	QSizeF paperSize;
	if (document->numPages() > 0) {
		Poppler::Page* pdfPage = document->page(0);  // Document starts at page 0
		if (!pdfPage) {
			QMessageBox::critical(this, "Save failed", "Could not open page 0.", QMessageBox::Ok);
			delete document;
			QFile::remove(saveName + ".under.pdf");
			QFile::remove(saveName + ".over.pdf");
			return;
		}
		paperSize = QSizeF(pdfPage->pageSizeF()) * (res/popplerDPI);
		delete pdfPage;
	}

	QPrinter printerRaster, printerScalableUnderlay, printerScalableOverlay;
	if (!scalable) {
		setupPrinter(printerRaster, saveName, res, paperSize);
	} else {
		setupPrinter(printerScalableUnderlay, saveName + ".under.pdf", res, paperSize);
		setupPrinter(printerScalableOverlay, saveName + ".over.pdf", res, paperSize);
	}

	QSwiftProgressDialog progressDialog("Writing file...", "Initializing...", 0, document->numPages() + 1, this);

	QPainter painterRaster, painterScalableUnderlay, painterScalableOverlay;
	if (!scalable) {
		painterRaster.begin(&printerRaster);
	} else {
		painterScalableUnderlay.begin(&printerScalableUnderlay);
		painterScalableOverlay.begin(&printerScalableOverlay);
	}
	QPainter *painter = 0;
	QPrinter *printer = scalable ? &printerScalableOverlay : &printerRaster;
	for (int pageNumber = 0; pageNumber < document->numPages(); pageNumber++) {
		renderAsync1(pageNumber);

		progressDialog.setValue(pageNumber);
		progressDialog.setText(QString("Rendering page %1 of %2...").arg(pageNumber+1).arg(document->numPages()));
		if (progressDialog.wasCanceled())
			break;

		Poppler::Page* pdfPage = document->page(pageNumber);  // Document starts at page 0
		if (!pdfPage) {
			continue;
		}

		if (pageNumber > 0) {
			if (!scalable) {
				printerRaster.newPage();
			} else {
				printerScalableUnderlay.newPage();
				printerScalableOverlay.newPage();
			}
		}

		QSizeF pageSize = pdfPage->pageSizeF();
		QRect paperRect = printer->paperRect();
		QSizeF resolution = QSizeF(popplerDPI * paperRect.width()/pageSize.width(), popplerDPI * paperRect.height()/pageSize.height());
		QImage image;
		QPainter imagePainter;
		if (!scalable) {
			image = pdfPage->renderToImage(resolution.width(), resolution.height(), paperRect.left(), paperRect.top(), paperRect.width(), paperRect.height());
			imagePainter.begin(&image);
		}

		if (!scalable) {
			painter = &imagePainter;
		} else {
			painter = &painterScalableOverlay;
		}

		foreach (PDFTextBox box, textBoxes[pageNumber]) {
			QRectF bboxText = QRectF(box.bbox.left() * paperRect.width(), box.bbox.top() * paperRect.height(), box.bbox.width() * paperRect.width(), box.bbox.height() * paperRect.height());

			bboxText.adjust(-2, -2, 2, 2);
			if (ui->btnReplaceFonts->isChecked()) {
				painter->fillRect(bboxText, Qt::white);
			}
			if (box.text == "andreas.mortensen@epfl.ch") {
				bboxText.adjust(0, 0, 2000, 0);
				painter->fillRect(bboxText, Qt::white);
				painter->setFont(QFont("Arial", 12, QFont::Normal));
				bboxText.adjust(2, 2, -2, -2);
				QRectF b = bboxText;
				//b.adjust(40, -5, 0, 0);
				QTextOption opt;
				opt.setWrapMode(QTextOption::NoWrap);
				painter->drawText(b, "research.office@epfl.ch / http://people.epfl.ch/andreas.mortensen", opt);
				bboxText.adjust(-2, -2, 2, 2);
				qDebug() << "Replaced text on page" << (pageNumber + 1);
			}
			if (box.isHighlighted) {
				if (!scalable) {
					painter = &imagePainter;
				} else {
					painter = &painterScalableUnderlay;
				}
				QPainter::CompositionMode oldMode = painter->compositionMode();
				if (!scalable) {
					painter->setCompositionMode(QPainter::CompositionMode_Multiply);
				}
				painter->fillRect(bboxText, box.highlightColor);
				// connect boxes
				if (box.nextId >= 0 && box.nextId < textBoxes[pageNumber].count()) {
					PDFTextBox nextBox = textBoxes[pageNumber][box.nextId];
					if (nextBox.isHighlighted) {
						QRectF nextBboxText = QRectF(nextBox.bbox.left() * paperRect.width(), nextBox.bbox.top() * paperRect.height(), nextBox.bbox.width() * paperRect.width(), nextBox.bbox.height() * paperRect.height());
						nextBboxText.adjust(-2, -2, 2, 2);
						if (nextBboxText.left() > bboxText.right()) {
							painter->fillRect(QRectF(bboxText.topRight(), bboxText.bottomRight() + QPointF(nextBboxText.left() - bboxText.right(), 0)),
											  box.highlightColor);
						}
					}
				}
				if (!scalable) {
					painter->setCompositionMode(oldMode);
				}
			}
			bboxText.adjust(2, 2, -2, -2);
		}

		// draw anchor targets
		foreach (TextAnchor a, anchors[pageNumber]) {
			QPointF widgetPos = QPointF(a.normalizedPos.x() * paperRect.width(), a.normalizedPos.y() * paperRect.height());

			if (localAnchorPixmaps.isEmpty()) {
				localAnchorPixmaps << QPixmap(":/icons/flag-blue-16.png");
				localAnchorPixmaps << QPixmap(":/icons/flag-blue-22.png");
				localAnchorPixmaps << QPixmap(":/icons/flag-blue-32.png");
				localAnchorPixmaps << QPixmap(":/icons/flag-blue-48.png");
			}
			if (interAnchorPixmaps.isEmpty()) {
				interAnchorPixmaps << QPixmap(":/icons/flag-blue-16.png");
				interAnchorPixmaps << QPixmap(":/icons/flag-blue-22.png");
				interAnchorPixmaps << QPixmap(":/icons/flag-blue-32.png");
				interAnchorPixmaps << QPixmap(":/icons/flag-blue-48.png");
			}
			bool local = a.docHash.isEmpty() || a.docHash == docHash;
			int optimalWidth = paperRect.width() / 30;
			int bestIndex = 0;
			if (local) {
				for (int i = 0; i < localAnchorPixmaps.count(); i++) {
					if (localAnchorPixmaps[i].width() <= optimalWidth) {
						bestIndex = i;
					}
				}
				if (!scalable) {
					painter = &imagePainter;
				} else {
					painter = &painterScalableOverlay;
				}
				painter->drawPixmap(widgetPos, localAnchorPixmaps[bestIndex]);
			}
		}

		// draw text annotations
		foreach (QObject *c, pixPages[pageNumber]->children()) {
			NoteWidget *nw = dynamic_cast<NoteWidget*>(c);
			if (!nw)
				continue;
			if (!scalable) {
				painter = &imagePainter;
			} else {
				painter = &painterScalableOverlay;
			}
			QPointF topLeft = widgetPos2NormalizedPos(pageNumber, nw->pos());
			topLeft.rx() *= paperRect.width();
			topLeft.ry() *= paperRect.height();
			//qreal sx, sy;
			//sx = res / nw->physicalDpiX() * 0.5;
			//sy = res / nw->physicalDpiY() * 0.5;
			//painter->scale(sx, sy);
			//topLeft.rx() *= 1/sx;
			//topLeft.ry() *= 1/sy;
			//nw->render(painter, topLeft.toPoint());
			//painter->scale(1/sx, 1/sy);
			painter->setFont(QFont("Times", 8, QFont::Bold));
			QRect textRect = painter->fontMetrics().tightBoundingRect(nw->getTitle());
			topLeft.ry() += 50;
			topLeft.rx() += 40;
			painter->fillRect(topLeft.x() + textRect.left() - 30,
							  topLeft.y() + textRect.y(),
							  textRect.width() + 30,
							  textRect.height(),
							  Qt::white);
			painter->drawText(topLeft, nw->getTitle());
			qDebug() << "Writing text on page" << (pageNumber + 1) << topLeft << textRect;
		}

		if (!scalable) {
			imagePainter.end();
			painterRaster.drawImage(0, 0, image);
		}
		delete pdfPage;
	}

	// draw report contents
	if (!ui->reportWidget->getTextEdit()->toPlainText().isEmpty()) {
		QTextDocument *doc = ui->reportWidget->getTextEdit()->document()->clone();
		doc->setTextWidth(printer->pageRect().width() * 0.8);

		doc->documentLayout()->setPaintDevice(printer);
		doc->setPageSize(printer->pageRect().size() * 0.8);

		int pageCount = qCeil(doc->size().height() / (printer->pageRect().height() * 0.8));
		progressDialog.setRange(0, document->numPages() + pageCount);
		for (int i = 0; i < pageCount; i++) {
			progressDialog.setValue(document->numPages() + i);
			progressDialog.setText(QString("Rendering page %1 of %2...").arg(document->numPages() + i + 1).arg(document->numPages() + pageCount));
			if (progressDialog.wasCanceled())
				break;

			QImage image = QImage(printer->paperRect().size(), QImage::Format_ARGB32_Premultiplied);

			QPainter p(&image);
			p.translate(printer->pageRect().width() * 0.1, -i * printer->pageRect().height() * 0.8 + printer->pageRect().height() * 0.1);
			doc->drawContents(&p);
			p.translate(-printer->pageRect().width() * 0.1, i * printer->pageRect().height() * 0.8 - printer->pageRect().height() * 0.1);
			p.fillRect(QRectF(0, 0, printer->pageRect().width(), printer->pageRect().height() * 0.1), Qt::white);
			p.fillRect(QRectF(0, 0, printer->pageRect().width() * 0.1, printer->pageRect().height()), Qt::white);
			p.fillRect(QRectF(0, printer->pageRect().height() * 0.9, printer->pageRect().width(), printer->pageRect().height() * 0.1), Qt::white);
			p.fillRect(QRectF(printer->pageRect().width() * 0.9, 0, printer->pageRect().width() * 0.1, printer->pageRect().height()), Qt::white);
			p.end();
			printer->newPage();
			if (!scalable) {
				painter = &painterRaster;
			} else {
				painter = &painterScalableOverlay;
			}
			painter->drawImage(0, 0, image);
		}
		delete doc;
	}

	if (!scalable) {
		painterRaster.end();
	} else {
		painterScalableUnderlay.end();
		painterScalableOverlay.end();
	}
	delete document;
	document = NULL;

	if (scalable) {
		progressDialog.setValue(progressDialog.maximum() - 1);
		progressDialog.setText("Writing output...");
		QProcess merger;
		merger.start("merge-pdfs.py", QStringList() <<
					 (saveName + ".under.pdf") <<
					 getFileName() <<
					 (saveName + ".over.pdf") <<
					 saveName);
		if (merger.waitForStarted(-1)) {
			merger.closeWriteChannel();
			if (merger.waitForFinished(-1)) {

				QString result = merger.readAll();
				qDebug() << result;
				if ((merger.exitStatus() != QProcess::NormalExit) ||
						(merger.exitStatus() == QProcess::NormalExit && merger.exitCode() != 0)) {
					QMessageBox::critical(this,
										  "Error",
										  QString("You might not have python installed and in your path, or the script "
												  "might fail processing this particular file.\n"
												  "\n"
												  "Script output:\n"
												  "\n"
												  "%1").arg(result),
										  QMessageBox::Ok);
				}
			}
		}
	}
	//QFile::remove(saveName + ".under.pdf");
	//QFile::remove(saveName + ".over.pdf");
}

void PDFViewer::redrawScrollBarBg()
{
	// redraw scrollbar bg
	MarkedScrollBar *ms = dynamic_cast<MarkedScrollBar *>(ui->scrollArea->verticalScrollBar());
	if (ms) {
		QImage image(ms->drawableRect().size(), QImage::Format_ARGB32);
		image.fill(Qt::darkGray);
		qreal scaleFactor = image.height()/qreal(ms->maximum() + ms->pageStep());
		{
			QPainter painter(&image);
			for (int pageNumber = 0; pageNumber < pixPages.count(); pageNumber++) {
				foreach (PDFTextBox box, textBoxes[pageNumber]) {
					if (box.isHighlighted) {
						QRectF widgetBox = normalizedRect2WidgetRect(pageNumber, box.bbox);
						widgetBox = widgetBox.adjusted(0, pixPages[pageNumber]->pos().y(), 0, pixPages[pageNumber]->pos().y());
						qreal barTop = widgetBox.top() * scaleFactor;
						qreal barBottom = widgetBox.bottom() * scaleFactor;
						if (barBottom - barTop < 3.0) {
							barTop = qRound(barTop) - 1;
							barBottom = barTop + 3;
						}
						painter.fillRect(0, barTop, image.width(), barBottom - barTop, QColor(box.highlightColor.red(), box.highlightColor.green(), box.highlightColor.blue()));
					}
				}
			}
			foreach (NoteWidget *nw, noteWidgets.uniqueKeys()) {
				int pageNumber = nw->getPageNumber();
				QPointF widgetPos = normalizedPos2WidgetPos(pageNumber, nw->getPos());
				widgetPos.ry() += pixPages[pageNumber]->pos().y();
				painter.fillRect(0, widgetPos.y() * scaleFactor, image.width(), 3, QColor(nw->getBgColor().red(), nw->getBgColor().green(), nw->getBgColor().blue()));
			}
		}
		ms->setBg(image);
	}
}

void PDFViewer::updatePageNumber()
{
	QString numText;
	if (viewMode == Slideshow) {
		QStackedLayout *layout = static_cast<QStackedLayout *>(ui->viewport->layout());
		if (layout) {
			int iPage = qMax(qMin(layout->currentIndex(), pixPages.count() - 1), 0);
			numText = QString("%1").arg(iPage + 1);
		}
	} else {
		int yCenter = ui->scrollArea->verticalScrollBar()->value() + ui->scrollArea->viewport()->height() / 2;
		QList<int> pageNumbers;
		for (int pageNumber = 0; pageNumber < pixPages.count(); pageNumber++) {
			int yStart = pixPages[pageNumber]->pos().y();
			int yEnd = pixPages[pageNumber]->pos().y() + pixPages[pageNumber]->height();
			if (yStart <= yCenter && yCenter <= yEnd) {
				pageNumbers << (pageNumber + 1);
			}
		}
		if (pageNumbers.isEmpty()) {
			pageNumbers << 0;
		}
		while (!pageNumbers.isEmpty()) {
			numText += QString(numText.isEmpty() ? "%1" : ", %1").arg(pageNumbers.takeFirst());
		}
	}
	ui->txtPageNumber->setText(QString("Page%3 %1 of %2").arg(numText).arg(pixPages.count()).arg(numText.contains(',') ? "s" : ""));
}


void PDFViewer::on_btnDocInfo_clicked()
{
	emit docInfoRequested(this, docHash);
}

void PDFViewer::tryWatchFile()
{
	if (fileName.isEmpty())
		return;
	fileWatcher.addPath(fileName);
	if (!fileWatcher.files().contains(fileName)) {
		QTimer::singleShot(1000, this, SLOT(tryWatchFile()));
	}
}

bool isPdfValid(QString fileName)
{
	Poppler::Document* temp = Poppler::Document::load(fileName);
	bool ok = temp;
	if (temp) {
		for (int i = 0; i < temp->numPages() && ok; i++) {
			Poppler::Page *page = temp->page(i);
			if (!page) {
				ok = false;
			} else {
				QSizeF size = page->pageSizeF();
				if (size.width() < 0 || size.height() < 0) {
					ok = false;
				}
			}
			delete page;
		}
	}
	delete temp;
	return ok;
}

void PDFViewer::onFileChanged(const QString &path)
{
	if (fileName != path)
		return;
	if (delayedFileChangedTriggered) {
		moreFileChangedTriggered = true;
		return;
	}
	delayedFileChangedTriggered = true;
	moreFileChangedTriggered = false;
	QTimer::singleShot(300, this, SLOT(onDelayedFileChanged()));
}

void PDFViewer::onDelayedFileChanged()
{
	if (moreFileChangedTriggered) {
		moreFileChangedTriggered = false;
		QTimer::singleShot(300, this, SLOT(onDelayedFileChanged()));
		return;
	}
	if (!isPdfValid(fileName)) {
		QTimer::singleShot(300, this, SLOT(onDelayedFileChanged()));
		return;
	}
	delayedFileChangedTriggered = false;
	reload();
}

void PDFViewer::reload()
{
	loadDocument(fileName, docHash, getRenderCache(), true);
}
